"""
The basic scheduler.

The scheduling done here takes the following factors into account:
 * Surrogate (peer) CPU power/strength.
 * Surrogate utilisation (as in the number of tasks currently 
   running on that surrogate).
 * Input size.
 * Communication media (i.e., network bandwidth).
 * Data locality (when remote data handles are used).
"""

from scheduler import Scheduler, ScheduleError
from scavenger_common.smrpc import SingleMindedProxy
from cPickle import dumps
from scavenger_common import RemoteDataHandle
from common import Candidate
    
class BasicScheduler(Scheduler):
    ASSUMED_COMPLEXITY = 50.0
                                   
    def __init__(self, context, scavenger):
        super(BasicScheduler, self).__init__(context, scavenger)

    def _get_datahandles(self, service_input):
        datahandles = []
        if type(service_input) == dict:
            # Keyword arguments.
            for item in service_input.values():
                if type(item) == RemoteDataHandle:
                    datahandles.append(item)
        elif type(service_input) in (tuple, list):
            # Positional arguments.
            for item in service_input:
                if type(item) == RemoteDataHandle:
                    datahandles.append(item)
        else:
            # Single argument.
            if type(service_input) == RemoteDataHandle:
                datahandles.append(service_input)

        return datahandles
        
    def schedule(self, service, local_cpu_strength, local_network_speed, local_activity, prefer_remote=False):
        # If no peers are available raise an exception to signal that local
        # execution should be performed.
        peers = self._context.get_peers()
        if len(peers) == 0:
            local_activity.increment()
            raise ScheduleError('No usable surrogates found.') 

        # Find the input size. 
        input_size = len(dumps(service.input, -1))
        # If service code is given its size must be added to the total input size.
        if service.code != None:
            input_size += len(service.code)

        # Get a list of data handles in the input.
        datahandles = self._get_datahandles(service.input)
            
        # Create the candidate list.
        candidates = []
        
        # Start by adding the local peer.
        if not prefer_remote:
            peer_strength = float(local_cpu_strength) / (local_activity.value + 1)
            time_to_perform = BasicScheduler.ASSUMED_COMPLEXITY / peer_strength

            time_to_transfer = 0
            for datahandle in datahandles:
                bandwidth = min(local_network_speed, self._context.get_peer(datahandle.server_address).net)
                time_to_transfer += (float(datahandle.size) / bandwidth)

            total_time = time_to_perform + time_to_transfer
            candidates.append(Candidate(total_time, None))
        
        # Then add remote peers.
        for peer in peers:
            # Find out how long it would take for the peer to perform the service.
            peer_strength = float(peer.cpu_strength)/(peer.active_tasks/peer.cpu_cores+1)
            time_to_perform = BasicScheduler.ASSUMED_COMPLEXITY / peer_strength
            
            # Find out how long it would take to transfer the input to the peer.
            time_to_transfer = float(input_size) / min(local_network_speed, peer.net) + 0.1 # 0.1 seconds of latency is added.
            for datahandle in datahandles:
                if datahandle.server_address != peer.name:
                    bandwidth = min(peer.net, self._context.get_peer(datahandle.server_address).net)
                    time_to_transfer += (float(datahandle.size) / bandwidth)

            total_time = time_to_perform + time_to_transfer
            candidates.append(Candidate(total_time, peer))
 
        # Sort the peers by the time it would take for them to perform the service.
        candidates.sort()

        # Perform the task.
        surrogate = candidates[0].peer

        # Check whether this is local execution.
        if surrogate == None:
            # By raising this exception we force the Scavenger lib to
            # do local execution.
            local_activity.increment()
            raise ScheduleError('Do local execution.')

        connection = SingleMindedProxy(surrogate.address)
        try:
            if not self._scavenger.has_service(surrogate, service.name, connection):
                self._scavenger.install_service(surrogate, service.name, service.code, connection)
            return self._scavenger.perform_service(surrogate, service.name, service.input, 
                                                   connection=connection, store=service.store)
        finally:
            try: connection.close() 
            except: pass
        
